﻿using Fleck;
using FySystem.Extension;
using FySystem.Logs;
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FyAutoStartService
{
    /// <summary>
    /// 网络文件传输工具类，为了防止粘包分包的问题，同样基于websocket，传输逻辑如下：
    /// 1.先由信息交互连接和客户端进行交互，确认好是需要接收文件还是发送文件，需要传输哪些文件，每个文件有一个GUID标识，
    /// 然后创建一个传输任务TransferTask，将其放入传输列表，创建TransferTask时传入参数：TransferToken和Files（需要传输的文件列表）
    /// 2.客户端通过信息交互连接收到TransferToken后，创建文件传输连接，并将TransferToken发送给服务端，如果需要多线程传输，可以建立多个连接；
    /// 发送TransferToken的格式：
    /// ---------------------------------
    /// 0x10 TransferToken
    /// ---------------------------------
    /// 3.服务端收到TransferToken之后可以确定这个连接和TransferTask的对应关系，然后将其与TransferTask绑定，然后回复消息
    /// ---------------------------------
    /// 0x10
    /// ---------------------------------
    /// 接着根据文件传输方向确定逻辑：
    /// 如果是从服务端->客户端：
    /// 3.1客户端创建好文件流，向服务器发送对应的FileGUID；
    ///     ---------------------------------
    ///     0x11 FileGUID
    ///     ---------------------------------
    /// 3.2服务端读取文件，然后逐包发送文件数据，第一版简单起见，不进行传输内容校验以及断线处理，如果要断线处理，则中断的文件整个重新传输，获取传输的时候记录当前位置
    ///     ---------------------------------
    ///     0x12 Position 最大8K的文件块
    ///     ---------------------------------
    /// 3.3客户端收到数据存入文件，并应答
    ///     ---------------------------------
    ///     0x13 FileGUID
    ///     ---------------------------------
    /// 3.4如果客户端收到数据之后处理错误，重复请求之前的块（暂未实现，后续做断线重连的时候再说）
    ///     ---------------------------------
    ///     0x14 FileGUID Position
    ///     ---------------------------------
    /// 3.5文件传输完毕，发送结束符
    ///     ---------------------------------
    ///     0x19
    ///     ---------------------------------
    ///     
    /// 如果是从客户端->服务端
    /// 3.1客户端向服务端发送即将要传输的文件GUID
    ///     ---------------------------------
    ///     0x21 FileGUID
    ///     ---------------------------------
    /// 3.2服务端收到GUID后找出对应的文件，并创建文件，然后向客户端发送文件请求
    ///     ---------------------------------
    ///     0x22 FileGUID
    ///     ---------------------------------
    /// 3.3客户端发送文件块
    ///     ---------------------------------
    ///     0x23 FileGUID Position 最大8K的文件块
    ///     ---------------------------------
    /// 3.4服务器收到文件块应答
    ///     ---------------------------------
    ///     0x22 FileGUID
    ///     ---------------------------------
    /// 3.5如果服务器收到文件后处理错误，重复请求之前的文件块（暂未实现）
    ///     ---------------------------------
    ///     0x24 FileGUID Position
    ///     ---------------------------------
    /// 3.3文件传输完毕，发送结束符
    ///     ---------------------------------
    ///     0x29 FileGUID
    ///     ---------------------------------
    /// 3.4服务端回应结束符
    ///     ---------------------------------
    ///     0x29 FileGUID
    ///     ---------------------------------
    ///     
    /// 如果传输过程中有出错，则发送（暂未实现）：
    /// ---------------------------------
    ///     0x31 错误信息
    /// ---------------------------------
    /// </summary>
    public class FileTransfer
    {
        /// <summary>
        /// 文件传输任务，key为TransferToken
        /// </summary>
        private Dictionary<string, TransferTask> _transferTaskes = new Dictionary<string, TransferTask>();

        public WebSocketServer WServer { get; private set; }

        /// <summary>
        /// 出错回调
        /// </summary>
        public Action<IWebSocketConnection, Exception> OnException { get; set; }

        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="wurl"></param>
        public FileTransfer(string wurl)
        {
            try
            {
                WServer = new WebSocketServer(wurl);

                Task.Run(() =>
                {
                    TransferTaskGC();
                });
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// 开始监听
        /// </summary>
        public void Start()
        {
            try
            {
                WServer.Start(socket =>
                {
                    //socket.OnOpen = () =>
                    //{

                    //};
                    //socket.OnClose = () =>
                    //{

                    //};
                    //socket.OnError = (ex) =>
                    //{
                    //    //RemoveSocket(socket);       //保险起见一旦出错就移除
                    //    OnException?.Invoke(socket, ex);
                    //};
                    socket.OnBinary = (barray) =>
                    {
                        ProcessBinary(socket, barray);
                    };
                });
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// 处理消息
        /// </summary>
        /// <param name="barray"></param>
        /// <exception cref="NotImplementedException"></exception>
        internal void ProcessBinary(IWebSocketConnection socket, byte[] barray)
        {
            try
            {
                if (barray[0] == 0x10)
                {
                    string transferToken = Encoding.UTF8.GetString(barray.AsSpan(1));
                    lock (_transferTaskes)
                    {
                        TransferTask transferTask = null;
                        if (_transferTaskes.TryGetValue(transferToken, out transferTask))
                        {
                            socket.OnBinary = barray =>
                            {
                                transferTask.ReceiveCallback(socket, barray, OnException);
                            };
                        }
                    }
                    socket.Send(new byte[] { 0x10 });
                }
            }
            catch (Exception ex)
            {
                OnException?.Invoke(socket, ex);
            }
        }

        /// <summary>
        /// 添加传输任务
        /// </summary>
        /// <param name="transferTask"></param>
        public void AddTransferTask(TransferTask transferTask)
        {
            try
            {
                if (transferTask == null)
                    throw new Exception("传入transferTask为空~");

                lock(_transferTaskes)
                {
                    if (_transferTaskes.TryAdd(transferTask.TransferToken, transferTask) == false)
                        throw new Exception("添加传输任务失败~");
                }
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// 递归获取子文件，文件传输专用
        /// </summary>
        /// <param name="di"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public static List<TransferFile> RecursionFiles(DirectoryInfo di, string rootPath)
        {
            List<TransferFile> res = new List<TransferFile>();
            try
            {
                DirectoryInfo[] subDirs = di.GetDirectories();
                if (subDirs.IsNullOrEmpty() == false)
                {
                    foreach (var item in subDirs)
                    {
                        List<TransferFile> subTs = RecursionFiles(item, rootPath);
                        if (subTs.IsNullOrEmpty() == false)
                            res.AddRange(subTs);
                    }
                }

                FileInfo[] subFiles = di.GetFiles();
                if (subFiles.IsNullOrEmpty() == false)
                {
                    foreach (var item in subFiles)
                    {
                        string relativeName = item.FullName.Replace('\\', '/');
                        if (!string.IsNullOrWhiteSpace(rootPath))
                        {
                            relativeName = relativeName.Substring(rootPath.Length);
                            if (relativeName.StartsWith('/') == false)
                                relativeName = $"/{relativeName}";
                        }

                        res.Add(new TransferFile(relativeName, item.FullName, item.Length));
                    }
                }
            }
            catch (Exception)
            {
                throw;
            }
            return res;
        }

        /// <summary>
        /// 垃圾回收
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        private void TransferTaskGC()
        {
            while (true)
            {
                try
                {
                    DateTime now = DateTime.Now;
                    lock (_transferTaskes)
                    {
                        List<string> keys = new List<string>();

                        foreach(string key in _transferTaskes.Keys)
                        {
                            var item = _transferTaskes[key];
                            if ((now - item.LastTransferTime).TotalSeconds >= 60)
                                keys.Add(key);
                        }

                        if (keys.Count > 0)
                        {
                            string strNow = now.ToString();
                            foreach(var key in keys)
                            {
                                var transferTask = _transferTaskes[key];
                                transferTask.Dispose();
                                _transferTaskes.Remove(key);
                                FyLogContextV2.WriteLog($"{strNow}:清理transferTask", key);
                            }
                        }
                    }
                }
                catch { }
                Thread.Sleep(60000);
            }
        }
    }

    /// <summary>
    /// 传输文件信息
    /// </summary>
    public class TransferFile
    {
        public string FileGUID { get; set; }

        /// <summary>
        /// 文件相对路径的文件名，如：/work/mywork.txt
        /// </summary>
        public string FileName { get; set; }

        /// <summary>
        /// 绝对路径
        /// </summary>
        internal string AbsoluteName { get; set; }

        /// <summary>
        /// 文件大小
        /// </summary>
        public long Size { get; set; }

        /// <summary>
        /// 是否存在
        /// </summary>
        public bool IsExists { get; set; }

        /// <summary>
        /// 文件流，上传文件的时候用
        /// </summary>
        internal FileStream Fs { get; set; }

        /// <summary>
        /// 当前文件位置，服务器->客户端时用
        /// </summary>
        internal long Position { get; set; }

        public TransferFile()
        { }

        /// <summary>
        /// 下载时用这个构造
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="absoluteName"></param>
        /// <param name="size"></param>
        public TransferFile(string fileName,string absoluteName,long size)
        {
            IsExists = true;
            FileGUID = Guid.NewGuid().ToString("N");
            FileName = fileName;
            AbsoluteName = absoluteName;
            Size = size;
        }

        /// <summary>
        /// 上传时用这个构造
        /// </summary>
        /// <param name="fileGuid"></param>
        /// <param name="absoluteName"></param>
        /// <param name="size"></param>
        public TransferFile(string fileGuid, string absoluteName)
        {
            IsExists = true;
            FileGUID = fileGuid;
            AbsoluteName = absoluteName;
        }
    }

    /// <summary>
    /// 传输任务
    /// </summary>
    public class TransferTask
    {
        private const int BLOCKSIZE = 8000;
        private int guidLength = 0;
        private ArrayPool<byte> arrayPool = ArrayPool<byte>.Create();

        /// <summary>
        /// 文件最后传输时间，这个是用于清理失效任务的
        /// </summary>
        internal DateTime LastTransferTime { get; set; } = DateTime.Now;

        /// <summary>
        /// 传输任务标识符
        /// </summary>
        public string TransferToken { get; private set; }

        /// <summary>
        /// 需要传输的文件，Key为FileGUID
        /// </summary>
        public ConcurrentDictionary<string, TransferFile> TransferFiles { get; private set; } = new ConcurrentDictionary<string, TransferFile>();

        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="transferToken"></param>
        /// <param name="files"></param>
        public TransferTask(string transferToken, List<TransferFile> files)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(transferToken))
                    throw new Exception("传入transferToken为空~");
                if (files.IsNullOrEmpty())
                    throw new Exception("传入files为空~");

                TransferToken = transferToken;
                foreach (var item in files)
                    TransferFiles.TryAdd(item.FileGUID, item);
            }
            catch (Exception)
            {
                throw;
            }
        }

        internal void ReceiveCallback(IWebSocketConnection socket, byte[] barray, Action<IWebSocketConnection, Exception> onException)
        {
            try
            {
                //更新最后通信时间
                LastTransferTime = DateTime.Now;

                if (barray[0] == 0x11)
                {
                    //客户端请求文件
                    string fileGUID = Encoding.UTF8.GetString(barray, 1, barray.Length - 1);

                    //查找对应的文件
                    TransferFile file = null;

                    if (TransferFiles.TryGetValue(fileGUID, out file))
                    {
                        if (file.Fs == null)
                            file.Fs = new FileStream(file.AbsoluteName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

                        file.Position = 0;

                        bool isEnd = SendFileBlock(file, BLOCKSIZE, socket);
                        if (isEnd)
                        {
                            socket.Send(new byte[] { 0x19 });
                            CloseFileStream(file.Fs);
                        }
                    }
                }
                else if (barray[0] == 0x13)
                {
                    //客户端收到文件块
                    string fileGUID = Encoding.UTF8.GetString(barray, 1, barray.Length - 1);

                    //查找对应的文件
                    TransferFile file = null;

                    if (TransferFiles.TryGetValue(fileGUID, out file))
                    {
                        bool isEnd = SendFileBlock(file, BLOCKSIZE, socket);
                        if (isEnd)
                        {
                            socket.Send(new byte[] { 0x19 });
                            CloseFileStream(file.Fs);
                        }
                    }
                }
                else if (barray[0] == 0x21)
                {
                    //文件GUID
                    string fileGUID = Encoding.UTF8.GetString(barray, 1, barray.Length - 1);

                    //由于同一个传输任务中的FileGUID都是同一个客户端产生的，因此长度都相同，将长度记录下来
                    guidLength = barray.Length - 1;

                    //查找对应文件
                    TransferFile file = null;

                    if (TransferFiles.TryGetValue(fileGUID, out file))
                    {
                        FileInfo fi = new FileInfo(file.AbsoluteName);
                        DirectoryInfo di = fi.Directory;
                        if (di.Exists == false)
                            di.Create();

                        if (file.Fs == null)
                            file.Fs = new FileStream(file.AbsoluteName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
                    }
                    else
                        throw new Exception("未找到文件~");

                    List<byte> data = new List<byte>();
                    data.Add(0x22);
                    data.AddRange(barray.AsSpan(1).ToArray());
                    socket.Send(data.ToArray());
                }
                else if (barray[0] == 0x23)
                {
                    //收到文件块
                    string fileGuid = Encoding.UTF8.GetString(barray.AsSpan(1, guidLength));

                    TransferFile file = null;
                    if (TransferFiles.TryGetValue(fileGuid, out file))
                    {
                        long position = BinaryPrimitives.ReadInt64BigEndian(barray.AsSpan(guidLength + 1, 8));
                        file.Fs.Seek(position, SeekOrigin.Begin);
                        file.Fs.Write(barray.AsSpan(guidLength + 9));

                        byte[] resp = barray.AsSpan(0, guidLength + 1).ToArray();
                        resp[0] = 0x22;
                        socket.Send(resp);
                    }
                    else
                        throw new Exception("未找到文件~");
                }
                else if (barray[0] == 0x29)
                {
                    //文件上传结束符
                    string fileGuid = Encoding.UTF8.GetString(barray.AsSpan(1, guidLength));

                    TransferFile file = null;
                    if (TransferFiles.TryGetValue(fileGuid, out file))
                    {
                        CloseFileStream(file.Fs);
                        socket.Send(barray);
                    }
                    else
                        throw new Exception("未找到文件~");
                }
            }
            catch (Exception ex)
            {
                onException?.Invoke(socket, ex);
            }
        }

        /// <summary>
        /// 发送文件块
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        private bool SendFileBlock(TransferFile file, int bLOCKSIZE, IWebSocketConnection socket)
        {
            try
            {
                if (file.Position >= file.Fs.Length)
                    return true;

                byte[] buffer = arrayPool.Rent(bLOCKSIZE+9);
                buffer[0] = 0x12;
                BitConverter.GetBytes(file.Position).CopyTo(buffer, 1);
                file.Fs.Seek(file.Position, SeekOrigin.Begin);
                int cnt = file.Fs.Read(buffer, 9, bLOCKSIZE);
                if (cnt <= 0)
                {
                    arrayPool.Return(buffer);
                    return true;
                }
                else
                {
                    socket.Send(buffer.AsSpan(0, cnt + 9).ToArray());

                    file.Position += cnt;
                    LastTransferTime = DateTime.Now;
                    arrayPool.Return(buffer);
                }
            }
            catch (Exception)
            {
                throw;
            }
            return false;
        }

        /// <summary>
        /// 关闭文件流
        /// </summary>
        /// <param name="fs"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void CloseFileStream(FileStream fs)
        {
            try
            {
                if (fs != null)
                {
                    fs.Close();
                    fs.Dispose();
                    fs = null;
                }
            }
            catch { }
        }

        internal void Dispose()
        {
            foreach (var val in TransferFiles.Values)
            {
                if (val.Fs != null)
                {
                    try
                    {
                        val.Fs.Dispose();
                    }
                    catch { }
                }
            }
        }
    }
}
